package pkl51.component;

import java.awt.*;
import java.beans.*;
import javax.swing.*;

/**
 *  The FormScroller will ensure that when a component gains focus it will
 *  always be visible within the viewport of the scrollpane.
 */
public class FormScroller extends JScrollPane implements PropertyChangeListener {

    public enum Type {

        COMPONENT,
        PARENT,
        CHILD;
    }
    private Type type;
    private Insets scrollInsets = new Insets(0, 0, 0, 0);
    private boolean scrollingEnabled;
    private Point lastLocation = new Point();

    /**
     *  Convenience constructor that set the scroll Type to COMPONENT
     */
    public FormScroller() {
        this(Type.COMPONENT);
    }

    /**
     *   Create a FormScroller for the specified scroll Type
     */
    public FormScroller(Type type) {
        setType(type);
        setScrollingEnabled(true);
    }

    /**
     *  Get the Type of scrolling to be attempted by the scroller
     */
    public Type getType() {
        return type;
    }

    /**
     *  Set the Type of scrolling to be done by the scroller
     *
     *  Type.COMPONENT - the focused component should be visible in the viewport.
     *  Type.PARENT - the parent Container of the focus component should be
     *                visible in the viewport. If the parent Container does not
     *                fit completely then use Type.COMPONENT.
     *  Type.CHILD - the child Contaier of the viewport view component which
     *               contains the focused component should be visible in the
     *               viewport. If the child Container does not fit completely
     *               then use Type.PARENT.
     *
     *  @param Type - controls scrolling of the viewport (values given above)
     */
    public void setType(Type type) {
        this.type = type;
    }

    /**
     *  Get the scroll insets.
     *
     *	@return the scroll insets
     */
    public Insets getScrollInsets() {
        return scrollInsets;
    }

    /**
     *  Set the scroll insets. The scroller will attempt to leave a gap between
     *  the scrolled Container and the edge of the scrollpane.
     *
     *  @paran scrollInsets - Insets for the gap for each edge of the scrollpane
     */
    public void setScrollInsets(Insets scrollInsets) {
        this.scrollInsets = scrollInsets;
    }

    /**
     *
     */
    public boolean isScrollingEnabled() {
        return scrollingEnabled;
    }

    /**
     *  Enable automatic scrolling on the form.
     *
     *  @param scrollingEnabled enable/disable scrolling
     */
    public void setScrollingEnabled(boolean scrollingEnabled) {
        this.scrollingEnabled = scrollingEnabled;

        KeyboardFocusManager.getCurrentKeyboardFocusManager().removePropertyChangeListener("permanentFocusOwner", this);

        if (scrollingEnabled) {
            KeyboardFocusManager.getCurrentKeyboardFocusManager().addPropertyChangeListener("permanentFocusOwner", this);
        }
    }

    @Override
    public void propertyChange(PropertyChangeEvent evt) {
        Component component = (Component) evt.getNewValue();

        if (component == null) {
            return;
        }

        //  Make sure the component with focus is in the viewport

        JComponent view = (JComponent) getViewport().getView();

        if (!SwingUtilities.isDescendingFrom(component, view)) {
            return;
        }

        //  Scroll the viewport

        Rectangle bounds = determineScrollBounds(component, view);
        view.scrollRectToVisible(bounds);
    }

    /**
     *  Determine the bounds that must fit into the viewport of the scrollpane
     *
     *  @param component the component that currently has focus
     *  @param view the component added to the viewport of the scrollpane
     *  @return a Rectangle representing the bounds to be scrolled
     */
    protected Rectangle determineScrollBounds(Component component, JComponent view) {
        //  Determine the scroll bounds based on the scroll Type

        Rectangle bounds = null;

        if (type == Type.COMPONENT) {
            bounds = determineComponentBounds(component, view);
        } else if (type == Type.PARENT) {
            bounds = determineParentBounds(component, view);
        } else {
            bounds = determineChildBounds(component, view);
        }

        //  Adjust bounds to take scroll insets into consideration

        Point location = component.getLocation();
        location = SwingUtilities.convertPoint(component.getParent(), location, view);

        if (location.x < lastLocation.x) {
            bounds.x -= scrollInsets.left;
        } else {
            bounds.width += scrollInsets.right;
        }

        if (location.y < lastLocation.y) {
            bounds.y -= scrollInsets.top;
        } else {
            bounds.height += scrollInsets.bottom;
        }

        lastLocation = location;

        return bounds;
    }

    private Rectangle determineComponentBounds(Component component, JComponent view) {
        // Use the bounds of the focused component

        Rectangle bounds = component.getBounds();
        bounds = SwingUtilities.convertRectangle(component.getParent(), bounds, view);

        return bounds;
    }

    private Rectangle determineParentBounds(Component component, JComponent view) {
        //  Use the bound of the parent Container

        Component parent = component.getParent();
        Rectangle bounds = parent.getBounds();
        bounds = SwingUtilities.convertRectangle(parent.getParent(), bounds, view);

        //  Make sure the Container will fit into the viewport

        if (rectangleFits(bounds)) {
            return bounds;
        } else {
            return determineComponentBounds(component, view);
        }
    }

    private Rectangle determineChildBounds(Component component, JComponent view) {
        //  Search each child Component of the view to find the Container which
        //  contains the component which current has focus

        Component child = null;

        for (Component c : view.getComponents()) {
            if (SwingUtilities.isDescendingFrom(component, c)) {
                child = c;
                break;
            }
        }

        Rectangle bounds = child.getBounds();
        bounds = SwingUtilities.convertRectangle(child.getParent(), bounds, view);

        //  Make sure this container will fit into the viewport

        if (rectangleFits(bounds)) {
            return bounds;
        } else {
            return determineParentBounds(component, view);
        }
    }

    private boolean rectangleFits(Rectangle bounds) {
        Dimension viewport = getViewport().getSize();

        if (bounds.width > viewport.width || bounds.height > viewport.height) {
            return false;
        } else {
            return true;
        }
    }
}
